/* * Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved. * * This file is part of the Jspresso framework. * * Jspresso is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Jspresso is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Jspresso. If not, see <http://www.gnu.org/licenses/>. */ package org.jspresso.framework.application.frontend.controller; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.security.Principal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.commons.lang3.LocaleUtils; import org.hibernate.HibernateException; import org.hibernate.exception.ConstraintViolationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.hibernate4.SessionFactoryUtils; import org.springframework.transaction.TransactionException; import org.jspresso.framework.action.ActionContextConstants; import org.jspresso.framework.action.ActionException; import org.jspresso.framework.action.IAction; import org.jspresso.framework.action.IActionHandler; import org.jspresso.framework.application.AbstractController; import org.jspresso.framework.application.backend.BackendControllerHolder; import org.jspresso.framework.application.backend.IBackendController; import org.jspresso.framework.application.backend.session.IApplicationSession; import org.jspresso.framework.application.frontend.IFrontendController; import org.jspresso.framework.application.frontend.action.FrontendAction; import org.jspresso.framework.application.frontend.action.workspace.ExitAction; import org.jspresso.framework.application.frontend.action.workspace.WorkspaceSelectionAction; import org.jspresso.framework.application.model.Module; import org.jspresso.framework.application.model.Workspace; import org.jspresso.framework.application.security.SecurityContextConstants; import org.jspresso.framework.application.view.descriptor.basic.WorkspaceCardViewDescriptor; import org.jspresso.framework.binding.ICollectionConnector; import org.jspresso.framework.binding.ICollectionConnectorListProvider; import org.jspresso.framework.binding.ICompositeValueConnector; import org.jspresso.framework.binding.IMvcBinder; import org.jspresso.framework.binding.IValueConnector; import org.jspresso.framework.binding.model.ModelRefPropertyConnector; import org.jspresso.framework.security.ISecurable; import org.jspresso.framework.security.ISecurityContextBuilder; import org.jspresso.framework.security.SecurityHelper; import org.jspresso.framework.security.UserPrincipal; import org.jspresso.framework.security.UsernamePasswordHandler; import org.jspresso.framework.util.descriptor.DefaultIconDescriptor; import org.jspresso.framework.util.event.IItemSelectable; import org.jspresso.framework.util.event.IItemSelectionListener; import org.jspresso.framework.util.event.ItemSelectionEvent; import org.jspresso.framework.util.exception.BusinessException; import org.jspresso.framework.util.gui.Dimension; import org.jspresso.framework.util.gui.Icon; import org.jspresso.framework.util.i18n.ITranslationProvider; import org.jspresso.framework.util.lang.ObjectUtils; import org.jspresso.framework.util.preferences.IPreferencesStore; import org.jspresso.framework.util.uid.RandomGUID; import org.jspresso.framework.util.url.UrlHelper; import org.jspresso.framework.view.IIconFactory; import org.jspresso.framework.view.IMapView; import org.jspresso.framework.view.IView; import org.jspresso.framework.view.IViewFactory; import org.jspresso.framework.view.action.ActionList; import org.jspresso.framework.view.action.ActionMap; import org.jspresso.framework.view.action.IDisplayableAction; import org.jspresso.framework.view.descriptor.IViewDescriptor; import org.jspresso.framework.view.descriptor.basic.BasicViewDescriptor; /** * Base class for frontend application controllers. Frontend controllers are * responsible for adapting the Jspresso application to the UI channel. Although * this generic abstract class centralizes most of the controller's * configuration, it will be subclassed by concrete, UI dependent subclasses to * implement polymorphic behaviour. * <p> * More than a behavioural adapter, the frontend controller will also be the * place where you define the top-level application structure like the workspace * list, the name, the application-wide actions, ... * * @param <E> * the actual gui component type used. * @param <F> * the actual icon type used. * @param <G> * the actual action type used. * @author Vincent Vandenschrick */ public abstract class AbstractFrontendController<E, F, G> extends AbstractController implements IFrontendController<E, F, G> { /** * {@code MAX_LOGIN_RETRIES}. */ protected static final int MAX_LOGIN_RETRIES = 3; private static final Logger LOG = LoggerFactory.getLogger(AbstractFrontendController.class); private static final String UP_KEY = "UP_KEY"; private static final String UP_SEP = "!"; private static final String UP_GUID = "UP_GUID"; private static final String LANG_KEY = "LANG_KEY"; private static final String TZ_KEY = "TZ_KEY"; private static final String CURR_DIALOG_VIEW = "CURR_DIALOG_VIEW"; private final List<ModuleHistoryEntry> backwardHistoryEntries; private final DefaultIconDescriptor controllerDescriptor; private final List<Map<String, Object>> dialogContextStack; private final List<ModuleHistoryEntry> forwardHistoryEntries; private final Map<String, IMapView<E>> workspaceViews; private final Map<String, Module> selectedModules; private final Map<String, ICompositeValueConnector> workspaceNavigatorConnectors; private boolean started; private ActionMap actionMap; private ActionMap secondaryActionMap; private Locale clientLocale; private IDisplayableAction exitAction; private String forcedStartingLocale; private ActionMap helpActionMap; private ActionMap navigationActionMap; private UsernamePasswordHandler loginCallbackHandler; private String loginContextName; private IViewDescriptor loginViewDescriptor; private boolean moduleAutoPinEnabled; private IMvcBinder mvcBinder; private IAction onModuleEnterAction; private IAction onModuleExitAction; private IAction onModuleStartupAction; private String selectedWorkspaceName; private IAction loginAction; private IAction startupAction; private boolean tracksWorkspaceNavigator; private IViewFactory<E, F, G> viewFactory; private Map<String, Workspace> workspaces; private String workspacesMenuIconImageUrl; private Integer frameWidth; private Integer frameHeight; private IPreferencesStore clientPreferencesStore; private boolean checkActionThreadSafety; private final PropertyChangeListener dirtInterceptor; private List<IAction> actionStack; /** * Constructs a new {@code AbstractFrontendController} instance. */ public AbstractFrontendController() { started = false; controllerDescriptor = new DefaultIconDescriptor(); selectedModules = new HashMap<>(); dialogContextStack = new ArrayList<>(); workspaceNavigatorConnectors = new HashMap<>(); workspaceViews = new HashMap<>(); backwardHistoryEntries = new LinkedList<>(); forwardHistoryEntries = new LinkedList<>(); moduleAutoPinEnabled = true; tracksWorkspaceNavigator = true; checkActionThreadSafety = true; actionStack = new ArrayList<>(); dirtInterceptor = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { Module module = getSelectedModule(); if (module != null && !module.isDirty()) { // Retrieve the top module while (module.getParent() != null) { module = module.getParent(); } module.refreshDirtinessInDepth(getBackendController()); } } }; } /** * {@inheritDoc} */ @Override public void displayModule(Module module) { displayModule(getModuleWorkspace(module), module); } /** * Determines the workspace name of the parameter module. * * @param module * the module to determine the workspace of. * @return the module workspace name. If no workspace already contains this * module, defaults to the current one. */ protected String getModuleWorkspace(Module module) { String selectedWsName = getSelectedWorkspaceName(); // Look first in the current WS if (belongsTo(getWorkspace(selectedWsName), module)) { return selectedWsName; } // Then on the others for (String wsName : getWorkspaceNames()) { if (!wsName.equals(selectedWsName) && belongsTo(getWorkspace(wsName), module)) { return wsName; } } return selectedWsName; } private boolean belongsTo(Workspace owner, Module module) { for (Module child : owner.getModules()) { if (belongsTo(child, module)) { return true; } } return false; } private boolean belongsTo(Module owner, Module module) { if (owner == module) { return true; } List<Module> subModules = owner.getSubModules(); if (subModules != null) { for (Module child : subModules) { if (belongsTo(child, module)) { return true; } } } return false; } /** * {@inheritDoc} */ @Override public void displayModule(String workspaceName, Module module) { Module currentModule = getSelectedModule(); // Test same workspace and same module. important when module is null and // selected module also to avoid stack overflows. String oldSelectedWorkspaceName = getSelectedWorkspaceName(); if (((oldSelectedWorkspaceName == null && workspaceName == null) || ObjectUtils.equals(oldSelectedWorkspaceName, workspaceName)) && ((currentModule == null && module == null) || ObjectUtils.equals(currentModule, module))) { if (currentModule != null && module != null && ObjectUtils.equals(currentModule.getParent(), module.getParent())) { return; } } if (currentModule != null) { pinModule(oldSelectedWorkspaceName, currentModule); Map<String, Object> navigationContext = getModuleActionContext(oldSelectedWorkspaceName); navigationContext.put(ActionContextConstants.FROM_MODULE, currentModule); navigationContext.put(ActionContextConstants.TO_MODULE, module); execute(currentModule.getExitAction(), navigationContext); execute(getOnModuleExitAction(), navigationContext); } displayWorkspace(workspaceName, true); IView<E> moduleAreaView = workspaceViews.get(workspaceName); if (moduleAreaView != null) { // The following does not seem necessary anymore. // This was done to cope with connectors mis-refreshing. // IValueConnector oldModuleModelConnector = moduleAreaViewConnector // .getModelConnector(); // if (oldModuleModelConnector != null) { // oldModuleModelConnector.setConnectorValue(null); // } IValueConnector moduleModelConnector = getBackendController().getModuleConnector(module); mvcBinder.bind(moduleAreaView.getConnector(), moduleModelConnector); } if (workspaceName != null) { selectedModules.put(workspaceName, module); } if (module != null) { if (!module.isStarted()) { if (getOnModuleStartupAction() != null) { execute(getOnModuleStartupAction(), getModuleActionContext(workspaceName)); } if (module.getStartupAction() != null) { execute(module.getStartupAction(), getModuleActionContext(workspaceName)); } } module.setStarted(true); String newSelectedWorkspaceName = getSelectedWorkspaceName(); pinModule(newSelectedWorkspaceName, module); Map<String, Object> navigationContext = getModuleActionContext(workspaceName); navigationContext.put(ActionContextConstants.FROM_MODULE, currentModule); navigationContext.put(ActionContextConstants.TO_MODULE, module); execute(getOnModuleEnterAction(), navigationContext); execute(module.getEntryAction(), navigationContext); } firePropertyChange(SELECTED_MODULE, currentModule, module); boolean wasTracksWorkspaceNavigator = tracksWorkspaceNavigator; try { tracksWorkspaceNavigator = false; ICompositeValueConnector workspaceNavigatorConnector = workspaceNavigatorConnectors.get(workspaceName); if (workspaceNavigatorConnector instanceof ICollectionConnectorListProvider) { Object[] result = synchWorkspaceNavigatorSelection( (ICollectionConnectorListProvider) workspaceNavigatorConnector, module); if (result != null) { int moduleModelIndex = (Integer) result[1]; ((ICollectionConnector) result[0]).setSelectedIndices(new int[]{moduleModelIndex}, moduleModelIndex); } } } finally { tracksWorkspaceNavigator = wasTracksWorkspaceNavigator; } } private Map<String, Object> getModuleActionContext(String workspaceName) { IMapView<E> moduleAreaView = workspaceViews.get(workspaceName); if (moduleAreaView != null) { IView<E> moduleView = moduleAreaView.getCurrentView(); if (moduleView != null) { return getViewFactory().getActionFactory().createActionContext(this, moduleView, moduleView.getConnector(), null, null); } } return new HashMap<>(); } /** * {@inheritDoc} */ @Override public void displayNextPinnedModule() { boolean wasAutoPinEnabled = moduleAutoPinEnabled; try { moduleAutoPinEnabled = false; if (forwardHistoryEntries.size() > 0) { ModuleHistoryEntry nextEntry = forwardHistoryEntries.remove(0); String nextWorkspaceName = nextEntry.getWorkspaceName(); Module nextModule = nextEntry.getModule(); if (nextWorkspaceName != null && nextModule != null) { backwardHistoryEntries.add(nextEntry); if (ObjectUtils.equals(nextWorkspaceName, getSelectedWorkspaceName()) && ObjectUtils.equals(nextModule, getSelectedModule())) { displayNextPinnedModule(); } else { displayModule(nextWorkspaceName, nextModule); pinnedModuleDisplayed(nextEntry, false); } } else { displayNextPinnedModule(); } } } finally { moduleAutoPinEnabled = wasAutoPinEnabled; } } /** * {@inheritDoc} */ @Override public void displayPreviousPinnedModule() { boolean wasAutoPinEnabled = moduleAutoPinEnabled; try { moduleAutoPinEnabled = false; if (backwardHistoryEntries.size() > 0) { ModuleHistoryEntry previousEntry = backwardHistoryEntries.remove(backwardHistoryEntries.size() - 1); String previousWorkspaceName = previousEntry.getWorkspaceName(); Module previousModule = previousEntry.getModule(); if (previousWorkspaceName != null && previousModule != null) { forwardHistoryEntries.add(0, previousEntry); if (ObjectUtils.equals(previousWorkspaceName, getSelectedWorkspaceName()) && ObjectUtils.equals( previousModule, getSelectedModule())) { displayPreviousPinnedModule(); } else { displayModule(previousWorkspaceName, previousModule); pinnedModuleDisplayed(previousEntry, false); } } else { displayPreviousPinnedModule(); } } } finally { moduleAutoPinEnabled = wasAutoPinEnabled; } } private String lastDisplayedSnapshotId; /** * Retrieves a pinned module in the backward or forward history and pins it. * * @param snapshotId * the snapshot id of the module history to display. * @return the history entry actually displayed or null if no change. */ protected ModuleHistoryEntry displayPinnedModule(String snapshotId) { if (!ObjectUtils.equals(snapshotId, lastDisplayedSnapshotId)) { lastDisplayedSnapshotId = snapshotId; ModuleHistoryEntry historyEntryToDisplay = null; for (ModuleHistoryEntry historyEntry : backwardHistoryEntries) { if (snapshotId.equals(historyEntry.getId())) { historyEntryToDisplay = historyEntry; } } if (historyEntryToDisplay != null) { while (backwardHistoryEntries.contains(historyEntryToDisplay)) { displayPreviousPinnedModule(); } return historyEntryToDisplay; } for (ModuleHistoryEntry historyEntry : forwardHistoryEntries) { if (snapshotId.equals(historyEntry.getId())) { historyEntryToDisplay = historyEntry; } } if (historyEntryToDisplay != null) { while (forwardHistoryEntries.contains(historyEntryToDisplay)) { displayNextPinnedModule(); } return historyEntryToDisplay; } } return null; } /** * {@inheritDoc} */ @Override public final void displayWorkspace(String workspaceName) { displayWorkspace(workspaceName, false); } /** * Displays a workspace. * * @param workspaceName * the workspace identifier. * @param bypassModuleBoundaryActions * should we bypass module onEnter/Exit actions ? */ protected void displayWorkspace(String workspaceName, boolean bypassModuleBoundaryActions) { if ((getSelectedWorkspaceName() == null && workspaceName == null) || ObjectUtils.equals(getSelectedWorkspaceName(), workspaceName)) { return; } Workspace workspace = null; boolean startingWorkspace = false; if (workspaceName != null) { workspace = getWorkspace(workspaceName); if (workspace != null) { startingWorkspace = !workspace.isStarted(); if (startingWorkspace) { workspace.setStarted(true); } } } if (bypassModuleBoundaryActions) { Workspace oldSelectedWorkspace = getSelectedWorkspace(); this.selectedWorkspaceName = workspaceName; firePropertyChange(SELECTED_WORKSPACE, oldSelectedWorkspace, getSelectedWorkspace()); } else { // do as if we had selected the module in the target workspace. // so that module boundary actions get triggered // see bug #538 displayModule(workspaceName, getSelectedModule(workspaceName)); } // Delay until the end of the very 1st execution. See bug #42. if (workspace != null && startingWorkspace && workspace.getStartupAction() != null) { Map<String, Object> actionContext = getInitialActionContext(); actionContext.put(ActionContextConstants.ACTION_PARAM, workspace); execute(workspace.getStartupAction(), actionContext); } } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public boolean disposeModalDialog(E sourceWidget, Map<String, Object> context) { LOG.debug("Disposing modal dialog."); if (dialogContextStack.size() > 0) { Map<String, Object> savedContext = dialogContextStack.remove(0); E currentDialogView = (E) savedContext.get(CURR_DIALOG_VIEW); if (currentDialogView != null && !isParentOf(currentDialogView, (IView<E>) context.get(ActionContextConstants.VIEW))) { dialogContextStack.add(0, savedContext); LOG.debug("Trying to dispose a dialog that is not the top one. Ignoring."); return false; } // preserve action param Object actionParam = context.get(ActionContextConstants.ACTION_PARAM); context.putAll(savedContext); context.put(ActionContextConstants.ACTION_PARAM, actionParam); return true; } else { LOG.debug("Trying to dispose a modal dialog while there is no dialog left."); } return false; } /** * Transfer focus. * * @param context * the context */ protected void transferFocus(Map<String, Object> context) { @SuppressWarnings("unchecked") E componentToFocus = (E) context.get(FrontendAction.COMPONENT_TO_FOCUS); if (componentToFocus != null) { focus(componentToFocus); } } private boolean isParentOf(E parentView, IView<E> view) { if (view == null) { return false; } if (parentView == view.getPeer()) { return true; } return isParentOf(parentView, view.getParent()); } private boolean executeDelayedActions = true; private boolean checkActionChainTheadSafety = true; /** * Executes frontend actions and delegates backend actions execution to its * peer backend controller. * <p/> * {@inheritDoc} */ @SuppressWarnings({"ThrowFromFinallyBlock", "ConstantConditions"}) @Override public boolean execute(IAction action, Map<String, Object> context) { if (action == null) { return true; } Map<String, Object> actionContext = getInitialActionContext(); // Retain only entries from the initial action context that are not in the // action context. actionContext.putAll(context); context.putAll(actionContext); // This is handled here since the selected module might have changed during // the action chain. context.put(ActionContextConstants.CURRENT_MODULE, getSelectedModule()); boolean result; Map<String, Object> initialActionState = null; boolean savedExecuteDelayedActions = executeDelayedActions; boolean savedCheckActionChainTheadSafety = checkActionChainTheadSafety; try { if (executeDelayedActions) { executeDelayedActions = false; executeDelayedActions(this); } if (isCheckActionThreadSafety() && checkActionChainTheadSafety) { checkActionChainTheadSafety = false; try { initialActionState = extractInternalActionState(action); } catch (IllegalAccessException ex) { throw new ActionException(ex, "Unable to extract internal action state for thread-safety checking of action : " + action); } } // Should be handled before getting there. // checkAccess(action); actionStack.add(0, action); if (action.isBackend()) { result = executeBackend(action, context); } else { result = executeFrontend(action, context); } if (!actionStack.isEmpty()) { actionStack.remove(0); } } catch (Throwable ex) { Throwable refinedException = ex; while (!actionStack.isEmpty()) { IAction callingAction = actionStack.remove(0); if (callingAction != null) { boolean handled = false; try { handled = callingAction.handleException(refinedException, context); } catch (Throwable reworkedException) { if (!actionStack.isEmpty()) { throw reworkedException; } refinedException = reworkedException; } if (handled) { return true; } } } handleException(refinedException, context); result = false; } finally { executeDelayedActions = savedExecuteDelayedActions; checkActionChainTheadSafety = savedCheckActionChainTheadSafety; if (initialActionState != null) { Map<String, Object> finalActionState; try { finalActionState = extractInternalActionState(action); } catch (IllegalAccessException ex) { throw new ActionException(ex, "Unable to extract internal action state for thread-safety checking of action : " + action); } if (!initialActionState.equals(finalActionState)) { LOG.error("A coding problem has been detected that breaks action thread-safety.\n" + "The action internal state has been modified during its execution which is strictly forbidden.\n" + "The action chain started with : {}", action); logInternalStateDifferences("root", initialActionState, finalActionState); throw new ActionException("A coding problem has been detected that breaks action thread-safety.\n" + "The action internal state has been modified during its execution which is strictly forbidden.\n" + "The action chain started with : " + action); } } } return result; } /** * Execute delayed actions. * * @param actionHandler * the action handler */ @Override public void executeDelayedActions(IActionHandler actionHandler) { ((AbstractController) getBackendController()).executeDelayedActions(this); super.executeDelayedActions(actionHandler); } @SuppressWarnings("unchecked") private void logInternalStateDifferences(String prefix, Map<String, Object> initialActionState, Map<String, Object> finalActionState) { for (Map.Entry<String, Object> initialEntry : initialActionState.entrySet()) { String leaf = initialEntry.getKey(); if (leaf.indexOf('.') >= 0) { leaf = leaf.substring(leaf.lastIndexOf('.') + 1); } String path = prefix + "|" + leaf; if (finalActionState.containsKey(initialEntry.getKey())) { Object initialValue = initialEntry.getValue(); Object finalValue = finalActionState.get(initialEntry.getKey()); if (initialValue != null && finalValue == null) { LOG.error(">> [{}] is not null in the initial action state but null in the final one.", path); } else if (initialValue == null && finalValue != null) { LOG.error(">> [{}] is null in the initial action state but not null in the final one.", path); } else if (initialValue != null && !initialValue.equals(finalValue)) { if (initialValue instanceof Map<?, ?> && finalValue instanceof Map<?, ?>) { logInternalStateDifferences(path, (Map<String, Object>) initialValue, (Map<String, Object>) finalValue); } else { LOG.error(">> [{}] is different in the initial action state and in the final one.", path); } } } else { LOG.error(">> [{}] is present in the final action state but not in the initial one.", path); } } for (Map.Entry<String, Object> finalEntry : finalActionState.entrySet()) { if (!initialActionState.containsKey(finalEntry.getKey())) { String leaf = finalEntry.getKey(); if (leaf.indexOf('.') >= 0) { leaf = leaf.substring(leaf.lastIndexOf('.') + 1); } String path = prefix + "|" + leaf; LOG.error(">> [{}] is present in the initial action state but not in the final one.", path); } } } /** * Gets the actions. * * @return the actions. */ @Override public ActionMap getActionMap() { return actionMap; } /** * {@inheritDoc} */ @Override public IApplicationSession getApplicationSession() { return getBackendController().getApplicationSession(); } /** * {@inheritDoc} */ @Override public IBackendController getBackendController() { return BackendControllerHolder.getCurrentBackendController(); } /** * {@inheritDoc} */ @Override public String getDescription() { return controllerDescriptor.getDescription(); } /** * Gets the help actions. * * @return the help actions. */ @Override public ActionMap getHelpActions() { return helpActionMap; } /** * Gets the navigation actions. * * @return the navigation actions. */ @Override public ActionMap getNavigationActions() { return navigationActionMap; } /** * {@inheritDoc} */ @Override public String getI18nDescription(ITranslationProvider translationProvider, Locale locale) { return controllerDescriptor.getI18nDescription(translationProvider, locale); } /** * {@inheritDoc} */ @Override public String getI18nName(ITranslationProvider translationProvider, Locale locale) { return controllerDescriptor.getI18nName(translationProvider, locale); } /** * {@inheritDoc} */ @Override public Icon getIcon() { return controllerDescriptor.getIcon(); } /** * Contains nothing. * <p> * {@inheritDoc} */ @Override public Map<String, Object> getInitialActionContext() { Map<String, Object> initialActionContext = new HashMap<>(); initialActionContext.putAll(getBackendController().getInitialActionContext()); for (int i = dialogContextStack.size() - 1; i >= 0; i--) { initialActionContext.putAll(dialogContextStack.get(i)); } initialActionContext.put(ActionContextConstants.FRONT_CONTROLLER, this); initialActionContext.put(ActionContextConstants.MODULE, selectedModules.get(getSelectedWorkspaceName())); return initialActionContext; } /** * {@inheritDoc} */ @Override public Locale getLocale() { if (getBackendController() != null) { Locale sessionLocale = getBackendController().getApplicationSession().getLocale(); if (sessionLocale != null) { return sessionLocale; } } if (getForcedStartingLocale() != null) { return LocaleUtils.toLocale(getForcedStartingLocale()); } return clientLocale; } /** * {@inheritDoc} */ @Override public TimeZone getClientTimeZone() { if (getBackendController() != null) { return getBackendController().getClientTimeZone(); } return TimeZone.getDefault(); } /** * {@inheritDoc} */ @Override public TimeZone getReferenceTimeZone() { if (getBackendController() != null) { return getBackendController().getReferenceTimeZone(); } return TimeZone.getDefault(); } /** * Gets the mvcBinder. * * @return the mvcBinder. */ @Override public IMvcBinder getMvcBinder() { return mvcBinder; } /** * {@inheritDoc} */ @Override public String getName() { return controllerDescriptor.getName(); } /** * {@inheritDoc} */ @Override public long getLastUpdated() { return controllerDescriptor.getLastUpdated(); } /** * Gets the selectedWorkspaceName. * * @return the selectedWorkspaceName. */ @Override public String getSelectedWorkspaceName() { return selectedWorkspaceName; } /** * {@inheritDoc} */ @Override public Workspace getSelectedWorkspace() { return getWorkspace(getSelectedWorkspaceName()); } @Override public Module getSelectedModule() { return getSelectedModule(getSelectedWorkspaceName()); } /** * {@inheritDoc} */ @Override public IAction getLoginAction() { return loginAction; } /** * {@inheritDoc} */ @Override public IAction getStartupAction() { return startupAction; } /** * Gets the viewFactory. * * @return the viewFactory. */ @Override public IViewFactory<E, F, G> getViewFactory() { return viewFactory; } /** * {@inheritDoc} */ @Override public Workspace getWorkspace(String workspaceName) { return getWorkspace(workspaceName, false); } /** * {@inheritDoc} */ @Override public Workspace getWorkspace(String workspaceName, boolean bypassSecurity) { if (workspaceName != null && workspaces != null) { Workspace workspace = workspaces.get(workspaceName); if (bypassSecurity || workspaceName.equals(getSelectedWorkspaceName()) || isAccessGranted(workspace)) { try { pushToSecurityContext(workspace); return workspace; } finally { restoreLastSecurityContextSnapshot(); } } } return null; } /** * {@inheritDoc} */ @Override public List<String> getWorkspaceNames() { return getWorkspaceNames(false); } /** * {@inheritDoc} */ @Override public List<String> getWorkspaceNames(boolean bypassSecurity) { if (workspaces != null) { List<String> workspaceNames = new ArrayList<>(); for (Map.Entry<String, Workspace> wsEntry : workspaces.entrySet()) { Workspace workspace = wsEntry.getValue(); if (bypassSecurity || isAccessGranted(workspace)) { try { pushToSecurityContext(workspace); workspaceNames.add(wsEntry.getKey()); } finally { restoreLastSecurityContextSnapshot(); } } } return workspaceNames; } return Collections.emptyList(); } /** * {@inheritDoc} */ @Override public void pinModule(Module module) { pinModule(getSelectedWorkspaceName(), module); } /** * Configures an application-wide action map that will be installed in the * main application frame. These actions are available at any time from the UI * and thus, do not depend on the active workspace. General purpose actions * like "Change password" action should be installed here. * * @param actionMap * the actionMap to set. */ public void setActionMap(ActionMap actionMap) { this.actionMap = actionMap; } /** * Sets the application description i18n key. The way this description is * actually leveraged depends on the UI channel. * * @param description * the description to set. */ public void setDescription(String description) { controllerDescriptor.setDescription(description); } /** * Configures the exit action to be executed whenever the user wants to quit * the application. The default installed exit action first checks for started * module(s) dirty state(s), then notifies user of pending persistent changes. * When no flush is needed or the user bypasses them, the actual exit is * performed. * * @param exitAction * the exitAction to set. */ public void setExitAction(IDisplayableAction exitAction) { this.exitAction = exitAction; } /** * Configures the locale used to initiate the login process. Whenever the * forced starting locale is {@code null}, the client host default locale * is used. * <p> * As soon as the user logs-in, his locale is then used to translate the UI. * Whenever the login process is disabled, then the forced starting locale is * kept as the UI i18n locale. * * @param forcedStartingLocale * the forcedStartingLocale to set. */ public void setForcedStartingLocale(String forcedStartingLocale) { this.forcedStartingLocale = forcedStartingLocale; } /** * Configures the help action map. The help action map should contain actions * that are related to helping the user (online help, reference manual, * tutorial, version dialog...). * <p> * The help action map is visually distinguished from the regular application * action map. For instance elp actions can be represented in a menu that is * right-aligned in the menu bar. * * @param helpActionMap * the helpActionMap to set. */ public void setHelpActionMap(ActionMap helpActionMap) { this.helpActionMap = helpActionMap; } /** * Configures the navigation action map. The navigation action map should * contain actions that are related to navigating the modules and workspace * history, e.g. previous, next, home, and so on. * * @param navigationActionMap * the navigationActionMap to set. */ public void setNavigationActionMap(ActionMap navigationActionMap) { this.navigationActionMap = navigationActionMap; } /** * Sets the icon image URL that is used as the application icon. Supported URL * protocols include : * <ul> * <li>all JVM supported protocols</li> * <li>the <b>jar:/</b> pseudo URL protocol</li> * <li>the <b>classpath:/</b> pseudo URL protocol</li> * </ul> * * @param iconImageURL * the iconImageURL to set. */ public void setIconImageURL(String iconImageURL) { controllerDescriptor.setIconImageURL(iconImageURL); } /** * Configures the name of the JAAS login context to use to authenticate users. * It must reference a valid JAAS context that is installed in the JVM, either * through setting the {@code java.security.auth.login.config} system * property or through server-specific configuration. * * @param loginContextName * the loginContextName to set. */ public void setLoginContextName(String loginContextName) { this.loginContextName = loginContextName; } /** * Configures the view descriptor used to create the login dialog. The default * built-in login view descriptor includes a standard login/password form. * * @param loginViewDescriptor * the loginViewDescriptor to set. */ public void setLoginViewDescriptor(IViewDescriptor loginViewDescriptor) { this.loginViewDescriptor = loginViewDescriptor; } /** * Configures the MVC binder used to apply model-view bindings. There is * hardly any reason for the developer to change the default binder but it * can be customized here. * * @param mvcBinder * the mvcBinder to set. */ public void setMvcBinder(IMvcBinder mvcBinder) { this.mvcBinder = mvcBinder; } /** * Sets the application name i18n key. The way this name is actually leveraged * depends on the UI channel but it typically generates (part of the) frame * title. * * @param name * the name to set. */ public void setName(String name) { controllerDescriptor.setName(name); } /** * Sets the lastUpdated. * * @param lastUpdated * the lastUpdated to set. * @internal */ public void setLastUpdated(long lastUpdated) { controllerDescriptor.setLastUpdated(lastUpdated); } /** * Configures an action to be executed each time a module of the application * is entered. The action is executed in the context of the module the user * enters. * * @param onModuleEnterAction * the onModuleEnterAction to set. */ public void setOnModuleEnterAction(IAction onModuleEnterAction) { this.onModuleEnterAction = onModuleEnterAction; } /** * Configures an action to be executed each time a module of the application * is started. The action is executed in the context of the module the user * starts. * * @param onModuleStartupAction * the onModuleStartupAction to set. */ public void setOnModuleStartupAction(IAction onModuleStartupAction) { this.onModuleStartupAction = onModuleStartupAction; } /** * Configures an action to be executed each time a module of the application * is exited. The action is executed in the context of the module the user * exits. Default frontend controller configuration installs an action that * checks current module dirty state. * * @param onModuleExitAction * the onModuleExitAction to set. */ public void setOnModuleExitAction(IAction onModuleExitAction) { this.onModuleExitAction = onModuleExitAction; } /** * Configures an action to be executed just after the user has successfully * logged-in but before any UI initialization has begun. An example of such an * action would be constructing a map of dynamic user right based on some * arbitrary data store so that the UI construction can actually depend on * these extracted values. * * @param loginAction * the loginAction to set. */ public void setLoginAction(IAction loginAction) { this.loginAction = loginAction; } /** * Configures an action to be executed on an empty UI context when the * application starts. The action executes once the user has logged-in and the * main UI has been constructed based on its access rights.An example of such * an action would be a default workspace/module opening and selection, a * "tip of the day" like action, ... * * @param startupAction * the startupAction to set. */ public void setStartupAction(IAction startupAction) { this.startupAction = startupAction; } /** * Configures the view factory used to create views from view descriptors. * Using a custom view factory is typically needed for extending Jspresso to * use custom view descriptors / UI components. Of course, there is a view * factory concrete type per UI channel. * * @param viewFactory * the viewFactory to set. */ public void setViewFactory(IViewFactory<E, F, G> viewFactory) { this.viewFactory = viewFactory; } /** * Configures the workspaces that are available in the application. Workspaces * are application entry-points and are hierarchically composed of modules / * sub-modules. * * @param workspaces * the workspaces to set. */ public void setWorkspaces(List<Workspace> workspaces) { this.workspaces = new LinkedHashMap<>(); for (Workspace workspace : workspaces) { this.workspaces.put(workspace.getName(), workspace); } } /** * Sets the icon image URL that is used as the workspace menu icon. Supported * URL protocols include : * <ul> * <li>all JVM supported protocols</li> * <li>the <b>jar:/</b> pseudo URL protocol</li> * <li>the <b>classpath:/</b> pseudo URL protocol</li> * </ul> * * @param workspacesMenuIconImageUrl * the workspacesMenuIconImageUrl to set. */ public void setWorkspacesMenuIconImageUrl(String workspacesMenuIconImageUrl) { this.workspacesMenuIconImageUrl = workspacesMenuIconImageUrl; } /** * Binds to the backend controller and ask it to start. * <p> * {@inheritDoc} */ @Override public boolean start(IBackendController peerController, Locale theClientLocale, TimeZone theClientTimeZone) { this.clientLocale = theClientLocale; Locale initialLocale = theClientLocale; if (forcedStartingLocale != null) { initialLocale = LocaleUtils.toLocale(forcedStartingLocale); } started = peerController.start(initialLocale, theClientTimeZone); peerController.addDirtInterceptor(dirtInterceptor); BackendControllerHolder.setSessionBackendController(peerController); return started; } /** * {@inheritDoc} */ @Override public boolean isStarted() { return started; } /** * {@inheritDoc} */ @Override public boolean stop() { if (getApplicationSession().getPrincipal() != null) { LOG.info("User {} logged out for session {}.", getApplicationSession().getUsername(), getApplicationSession().getId()); } selectedModules.clear(); workspaceNavigatorConnectors.clear(); workspaceViews.clear(); backwardHistoryEntries.clear(); forwardHistoryEntries.clear(); dialogContextStack.clear(); selectedWorkspaceName = null; loginCallbackHandler = null; getBackendController().removeDirtInterceptor(dirtInterceptor); started = !getBackendController().stop(); clearImplicitLogin(); return !started; } /** * Clear implicit principal, i.e. SSO principal or "remember me" login. */ protected void clearImplicitLogin() { String username = getApplicationSession().getUsername(); if (!SecurityHelper.ANONYMOUS_USER_NAME.equals(username)) { if (getClientPreference(UP_KEY) != null) { // reset get through pass rememberLogin(username, null); } } else { UsernamePasswordHandler uph = getLoginCallbackHandler(); uph.clear(); } removeUserPreference(UP_GUID); } /** * Remember login. * * @param username * the username * @param password * the password */ @Override public void rememberLogin(String username, String password) { putClientPreference(UP_KEY, encodeUserPass(username, password)); } /** * Gets remembered login. * * @return the remembered login */ @Override public String getRememberedLogin() { String[] savedUserPass = decodeUserPass(getClientPreference(UP_KEY)); if (savedUserPass != null && savedUserPass.length > 0) { return savedUserPass[0]; } return null; } /** * Creates a new login callback handler. * * @return a new login callback handler */ protected UsernamePasswordHandler createLoginCallbackHandler() { UsernamePasswordHandler uph = createUsernamePasswordHandler(); String[] savedUserPass = decodeUserPass(getClientPreference(UP_KEY)); if (savedUserPass != null && savedUserPass.length == 2 && savedUserPass[0] != null) { uph.setUsername(savedUserPass[0]); uph.setPassword(savedUserPass[1]); uph.setRememberMe(true); } else { uph.setUsername(null); uph.setPassword(null); uph.setRememberMe(false); } String savedLang = getClientPreference(LANG_KEY); if (savedLang != null && !savedLang.isEmpty()) { uph.setLanguage(savedLang); } else { uph.setLanguage(null); } String savedTzId = getClientPreference(TZ_KEY); if (savedTzId != null && !savedTzId.isEmpty()) { uph.setTimeZoneId(savedTzId); } else { uph.setTimeZoneId(null); } return uph; } /** * Creates a UsernamePassword handler instance. * * @return a new UsernamePassword handler instance. */ protected UsernamePasswordHandler createUsernamePasswordHandler() { UsernamePasswordHandler uph = new UsernamePasswordHandler(); return uph; } /** * Reads a client preference. * * @param key * the key under which the preference as been stored. * @return the stored preference or null. */ @Override public String getClientPreference(String key) { if (getClientPreferencesStore() != null) { return getClientPreferencesStore().getPreference(key); } return null; } /** * Stores a client preference. * * @param key * the key under which the preference as to be stored. * @param value * the value of the preference to be stored. */ @Override public void putClientPreference(String key, String value) { if (getClientPreferencesStore() != null) { getClientPreferencesStore().putPreference(key, value); } } /** * Deletes a client preference. * * @param key * the key under which the preference is stored. */ @Override public void removeClientPreference(String key) { if (getClientPreferencesStore() != null) { getClientPreferencesStore().removePreference(key); } } /** * Creates and binds the login view. * * @return the login view */ protected IView<E> createLoginView() { IViewDescriptor loginViewDescriptor = getLoginViewDescriptor(); BasicViewDescriptor refinedViewDescriptor = ((BasicViewDescriptor) loginViewDescriptor).clone(); refinedViewDescriptor.setActionMap(null); refinedViewDescriptor.setSecondaryActionMap(null); IView<E> loginView = getViewFactory().createView(refinedViewDescriptor, this, getLocale()); IValueConnector loginModelConnector = getBackendController().createModelConnector("login", loginViewDescriptor.getModelDescriptor()); getMvcBinder().bind(loginView.getConnector(), loginModelConnector); loginModelConnector.setConnectorValue(getLoginCallbackHandler()); return loginView; } /** * Creates the module area view to display the modules content. * * @param workspaceName * the workspace to create the module area view for. * @return the the module area view to display the modules content. */ protected IView<E> createModuleAreaView(String workspaceName) { IMapView<E> moduleAreaView = (IMapView<E>) viewFactory.createView(createWorkspaceViewDescriptor(), this, getLocale()); workspaceViews.put(workspaceName, moduleAreaView); return moduleAreaView; } /** * Create workspace view descriptor workspace card view descriptor. * * @return the workspace card view descriptor */ protected WorkspaceCardViewDescriptor createWorkspaceViewDescriptor() { return new WorkspaceCardViewDescriptor(); } /** * Creates the workspace action list. * * @return the workspace action list. */ protected ActionList createWorkspaceActionList() { ActionList workspaceSelectionActionList = new ActionList(); workspaceSelectionActionList.setName("workspaces"); workspaceSelectionActionList.setIconImageURL(getWorkspacesMenuIconImageUrl()); List<IDisplayableAction> workspaceSelectionActions = new ArrayList<>(); for (String workspaceName : getWorkspaceNames()) { Workspace workspace = getWorkspace(workspaceName); if (isAccessGranted(workspace)) { try { pushToSecurityContext(workspace); IViewDescriptor workspaceViewDescriptor = getWorkspace(workspaceName).getViewDescriptor(); WorkspaceSelectionAction<E, F, G> workspaceSelectionAction = createWorkspaceSelectionAction(workspaceName, workspaceViewDescriptor); workspaceSelectionActions.add(workspaceSelectionAction); } finally { restoreLastSecurityContextSnapshot(); } } } workspaceSelectionActionList.setActions(workspaceSelectionActions); workspaceSelectionActionList.setCollapsable(true); return workspaceSelectionActionList; } /** * Create workspace selection action workspace selection action. * * @param workspaceName * the workspace name * @param workspaceViewDescriptor * the workspace view descriptor * @return the workspace selection action */ protected WorkspaceSelectionAction<E, F, G> createWorkspaceSelectionAction(String workspaceName, IViewDescriptor workspaceViewDescriptor) { WorkspaceSelectionAction<E, F, G> workspaceSelectionAction = new WorkspaceSelectionAction<>(); workspaceSelectionAction.setWorkspaceName(workspaceName); workspaceSelectionAction.setName(workspaceViewDescriptor.getName()); workspaceSelectionAction.setDescription(workspaceViewDescriptor.getDescription()); workspaceSelectionAction.setIcon(workspaceViewDescriptor.getIcon()); return workspaceSelectionAction; } /** * Creates the workspace action map. * * @return the workspace action map. */ protected ActionMap createWorkspaceActionMap() { ActionMap workspaceActionMap = new ActionMap(); List<ActionList> workspaceActionLists = new ArrayList<>(); ActionList exitActionList = new ActionList(); exitActionList.setName("file"); exitActionList.setIconImageURL(getWorkspacesMenuIconImageUrl()); List<IDisplayableAction> exitActions = new ArrayList<>(); exitActions.add(getExitAction()); exitActionList.setActions(exitActions); workspaceActionLists.add(createWorkspaceActionList()); workspaceActionLists.add(exitActionList); workspaceActionMap.setActionLists(workspaceActionLists); return workspaceActionMap; } /** * Creates a workspace navigator based on the workspace definition. * * @param workspaceName * the workspace to create the navigator for. * @param workspaceNavigatorViewDescriptor * the view descriptor of the navigator. * @return the workspace navigator view. */ protected IView<E> createWorkspaceNavigator(final String workspaceName, IViewDescriptor workspaceNavigatorViewDescriptor) { IView<E> workspaceNavigatorView = viewFactory.createView(workspaceNavigatorViewDescriptor, this, getLocale()); IItemSelectable workspaceNavigator; if (workspaceNavigatorView.getConnector() instanceof IItemSelectable) { workspaceNavigator = (IItemSelectable) workspaceNavigatorView.getConnector(); } else { workspaceNavigator = (IItemSelectable) ((ICompositeValueConnector) workspaceNavigatorView.getConnector()) .getChildConnector(ModelRefPropertyConnector.THIS_PROPERTY); } workspaceNavigator.addItemSelectionListener(new IItemSelectionListener() { @Override public void selectedItemChange(ItemSelectionEvent event) { navigatorSelectionChanged(workspaceName, (ICompositeValueConnector) event.getSelectedItem()); } }); workspaceNavigatorConnectors.put(workspaceName, (ICompositeValueConnector) workspaceNavigatorView.getConnector()); return workspaceNavigatorView; } /** * Must be called when a modal dialog is displayed. * * @param dialogView * the dialog view * @param context * the context to store on the context stack. * @param reuseCurrent * set to true to reuse an existing modal dialog. */ protected void displayModalDialog(E dialogView, Map<String, Object> context, boolean reuseCurrent) { if (reuseCurrent && dialogContextStack.size() >= 1) { dialogContextStack.remove(0); } LOG.debug("Popping-up modal dialog."); context.put(CURR_DIALOG_VIEW, dialogView); dialogContextStack.add(0, context); } /** * Executes a backend action. * * @param action * the backend action to execute. * @param context * the action execution context. * @return true if the action was successfully executed. */ protected boolean executeBackend(IAction action, Map<String, Object> context) { return getBackendController().execute(action, context); } /** * Executes a frontend action. * * @param action * the frontend action to execute. * @param context * the action execution context. * @return true if the action was successfully executed. */ protected boolean executeFrontend(IAction action, Map<String, Object> context) { return action.execute(this, context); } /** * Whenever the loginContextName is not configured, creates a default subject. * * @return the default Subject in case the login configuration is not set. */ protected Subject getAnonymousSubject() { return SecurityHelper.createAnonymousSubject(); } /** * Creates the exit action. * * @return the exit action. */ @Override public IDisplayableAction getExitAction() { if (exitAction == null) { ExitAction<E, F, G> action = new ExitAction<>(); action.setName("quit.name"); action.setDescription("quit.description"); action.setIconImageURL(getViewFactory().getIconFactory().getCancelIconImageURL()); exitAction = action; } return exitAction; } /** * Gets the forcedStartingLocale. * * @return the forcedStartingLocale. */ protected String getForcedStartingLocale() { return forcedStartingLocale; } /** * Gets the iconFactory. * * @return the iconFactory. */ protected IIconFactory<F> getIconFactory() { return viewFactory.getIconFactory(); } /** * Gets the loginCallbackHandler. * * @return the loginCallbackHandler. */ protected UsernamePasswordHandler getLoginCallbackHandler() { if (loginCallbackHandler == null) { loginCallbackHandler = createLoginCallbackHandler(); } return loginCallbackHandler; } /** * Gets the loginContextName. * * @return the loginContextName. */ protected String getLoginContextName() { return loginContextName; } /** * Should a login dialog be displayed or should we process login implicitly * (either through SSO or using an anonymous subject in case of un-protected * application). * * @return true if {@code getLoginContext()} returns null. */ protected boolean isLoginInteractive() { return getLoginContextName() != null && !shouldAutoLogin(); } /** * Gets the loginViewDescriptor. * * @return the loginViewDescriptor. */ protected IViewDescriptor getLoginViewDescriptor() { return loginViewDescriptor; } /** * Gets the onModuleEnterAction. * * @return the onModuleEnterAction. */ protected IAction getOnModuleEnterAction() { return onModuleEnterAction; } /** * Gets the onModuleStartupAction. * * @return the onModuleStartupAction. */ protected IAction getOnModuleStartupAction() { return onModuleStartupAction; } /** * Gets the onModuleExitAction. * * @return the onModuleExitAction. */ protected IAction getOnModuleExitAction() { return onModuleExitAction; } /** * Gets the selected module. * * @param workspaceName * the workspace name to query the selected module for. * @return the selected module. */ protected Module getSelectedModule(String workspaceName) { return selectedModules.get(workspaceName); } /** * Gets the workspacesMenuIconImageUrl. * * @return the workspacesMenuIconImageUrl. */ protected String getWorkspacesMenuIconImageUrl() { return workspacesMenuIconImageUrl; } /** * {@inheritDoc} */ @Override public void loggedIn(Subject subject) { UsernamePasswordHandler uph = getLoginCallbackHandler(); if (!SecurityHelper.ANONYMOUS_USER_NAME.equals(uph.getUsername())) { if (uph.isRememberMe()) { rememberLogin(uph.getUsername(), uph.getPassword()); } else { removeClientPreference(UP_KEY); } } String loginLocale = uph.getLanguage(); if (loginLocale != null && !loginLocale.isEmpty()) { putClientPreference(LANG_KEY, loginLocale); } else { removeClientPreference(LANG_KEY); } String loginTimeZoneId = uph.getTimeZoneId(); if (loginTimeZoneId != null && !loginTimeZoneId.isEmpty()) { putClientPreference(TZ_KEY, loginTimeZoneId); } else { removeClientPreference(TZ_KEY); } uph.clear(); if (loginLocale != null) { for (Principal principal : subject.getPrincipals()) { if (principal instanceof UserPrincipal) { ((UserPrincipal) principal).putCustomProperty(UserPrincipal.LANGUAGE_PROPERTY, loginLocale); } } } if (loginTimeZoneId != null) { TimeZone loginTimeZone = TimeZone.getTimeZone(loginTimeZoneId); if (loginTimeZone != null) { getBackendController().setClientTimeZone(loginTimeZone); } } getBackendController().loggedIn(subject); execute(getLoginAction(), getLoginActionContext()); if (workspaces != null) { Map<String, Workspace> filteredWorkspaces = new HashMap<>(); for (Map.Entry<String, Workspace> workspaceEntry : workspaces.entrySet()) { Workspace workspace = workspaceEntry.getValue(); // Must be put here so that ws that are not accessible // due to security restrictions are still translated. translateWorkspace(workspace); if (isAccessGranted(workspace)) { try { pushToSecurityContext(workspace); workspace.setSecurityHandler(this); translateWorkspace(workspace); filteredWorkspaces.put(workspaceEntry.getKey(), workspaceEntry.getValue()); } finally { restoreLastSecurityContextSnapshot(); } } } getBackendController().installWorkspaces(filteredWorkspaces); } } /** * Constructs the context to call the login action. Defaults to * {@link AbstractFrontendController#getInitialActionContext()}. * * @return the login action context. */ protected Map<String, Object> getLoginActionContext() { return getInitialActionContext(); } /** * Constructs the context to call the startup action. Defaults to * {@link AbstractFrontendController#getInitialActionContext()}. * * @return the startup action context. */ protected Map<String, Object> getStartupActionContext() { return getInitialActionContext(); } /** * Request anonymous login to tha application. */ @Override public void loginAnonymously() { UsernamePasswordHandler uph = getLoginCallbackHandler(); uph.setUsername(SecurityHelper.ANONYMOUS_USER_NAME); uph.setPassword(""); login(); } /** * Perform JAAS login. * * @return the logged-in subject or null if login failed. */ protected Subject performJAASLogin() { CallbackHandler lch = getLoginCallbackHandler(); try { LoginContext lc; try { lc = new LoginContext(getLoginContextName(), lch); } catch (LoginException le) { LOG.error("Cannot create LoginContext.", le); return null; } catch (SecurityException se) { LOG.error("Cannot create LoginContext.", se); return null; } lc.login(); return lc.getSubject(); } catch (LoginException le) { // le.getCause() is always null, so cannot rely on it. // see bug #1019 if (!(le instanceof FailedLoginException)) { String message = le.getMessage(); if (message.indexOf(':') > 0) { String exceptionClassName = message.substring(0, message.indexOf(':')); try { if (Throwable.class.isAssignableFrom(Class.forName(exceptionClassName))) { LOG.error("A technical exception occurred on login module.", le); } } catch (ClassNotFoundException ignored) { // ignored. } } } return null; } } /** * Performs the actual JAAS login. * * @return true if login succeeded. */ protected boolean performLogin() { Subject subject; String lcName = getLoginContextName(); if (lcName != null) { subject = performJAASLogin(); } else { subject = getAnonymousSubject(); } if (subject == null) { LOG.info("User {} failed to log in for session {}.", getLoginCallbackHandler().getUsername(), getApplicationSession().getId()); return false; } LOG.info("User {} logged in for session {}.", getLoginCallbackHandler().getUsername(), getApplicationSession().getId()); loggedIn(subject); return true; } /** * Pins a module in the history navigation thus allowing the user to navigate * back. * * @param workspaceName * the workspace to pin the module for. * @param module * the module to pin. */ protected void pinModule(String workspaceName, Module module) { if (moduleAutoPinEnabled && module != null) { if (backwardHistoryEntries.size() > 0) { ModuleHistoryEntry lastPinnedModule = backwardHistoryEntries.get(backwardHistoryEntries.size() - 1); if (lastPinnedModule.getWorkspaceName().equals(workspaceName) && lastPinnedModule.getModule().equals(module)) { return; } } String historyEntryName = getWorkspace(workspaceName).getI18nName() + " - " + module.getI18nName(); ModuleHistoryEntry historyEntry = new ModuleHistoryEntry(workspaceName, module, historyEntryName); backwardHistoryEntries.add(historyEntry); pinnedModuleDisplayed(historyEntry, true); forwardHistoryEntries.clear(); } } /** * Callback when a module is actually pinned in history. * * @param historyEntry * the pinned module history entry. * @param addToHistory * the add to history */ protected void pinnedModuleDisplayed(ModuleHistoryEntry historyEntry, boolean addToHistory) { // NO-OP. Managed by subclasses when needed. } /** * Refines the data integrity violation exception to determine the translation * key from which the user message will be constructed. * * @param exception * the DataIntegrityViolationException. * @return the translation key to use. */ protected String refineIntegrityViolationTranslationKey(DataIntegrityViolationException exception) { if (exception.getCause() instanceof ConstraintViolationException) { ConstraintViolationException cve = (ConstraintViolationException) exception.getCause(); if (cve.getSQL() != null && cve.getSQL().toUpperCase().contains("DELETE")) { return "error.fk.delete"; } if (cve.getConstraintName() != null) { if (cve.getConstraintName().toUpperCase().contains("FK")) { return "error.fk.update"; } return "error.unicity"; } return "error.integrity"; } return "error.integrity"; } /** * Computes a user friendly exception message if this exception is known and * can be cleanly handled by the framework. * * @param exception * the exception to compute the message for. * @return the user friendly message or null if this exception is unexpected. */ protected String computeUserFriendlyExceptionMessage(Throwable exception) { Throwable refinedException = exception; if (refinedException instanceof TransactionException) { Throwable cause = refinedException.getCause(); while (cause != null) { if (cause instanceof HibernateException) { refinedException = cause; break; } else if (cause instanceof SecurityException) { refinedException = cause; break; } else if (cause instanceof BusinessException) { refinedException = cause; break; } else if (cause instanceof DataIntegrityViolationException) { refinedException = cause; break; } else if (cause instanceof ConcurrencyFailureException) { refinedException = cause; break; } cause = cause.getCause(); } } if (refinedException instanceof HibernateException) { refinedException = SessionFactoryUtils.convertHibernateAccessException((HibernateException) refinedException); } if (refinedException instanceof SecurityException) { return refinedException.getMessage(); } if (refinedException instanceof BusinessException) { return ((BusinessException) refinedException).getI18nMessage(this, getLocale()); } if (refinedException instanceof DataIntegrityViolationException) { String constraintTranslation = null; if (refinedException.getCause() instanceof ConstraintViolationException) { ConstraintViolationException cve = ((ConstraintViolationException) refinedException.getCause()); if (cve.getConstraintName() != null) { constraintTranslation = getTranslation(cve.getConstraintName(), getLocale()); } } if (constraintTranslation == null) { constraintTranslation = getTranslation("unknown", getLocale()); } return getTranslation(refineIntegrityViolationTranslationKey((DataIntegrityViolationException) refinedException), new Object[]{constraintTranslation}, getLocale()); } if (refinedException instanceof ConcurrencyFailureException) { return getTranslation("concurrency.error.description", getLocale()); } if (refinedException instanceof TransactionException) { LOG.error("The transaction was unexpectedly rolled back", refinedException); Class<?> exceptionClass = refinedException.getClass(); Throwable cause = refinedException.getCause(); if (cause != null) { exceptionClass = cause.getClass(); } return getTranslation("transaction.error.description", new String[]{exceptionClass.getSimpleName(), refinedException.getMessage()}, getLocale()); } return null; } private void navigatorSelectionChanged(String workspaceName, ICompositeValueConnector selectedConnector) { if (tracksWorkspaceNavigator) { handleWorkspaceNavigatorSelection(workspaceName, selectedConnector); } } /** * Handle workspace navigator selection. * * @param workspaceName * the workspace name * @param selectedConnector * the selected connector */ protected void handleWorkspaceNavigatorSelection(String workspaceName, ICompositeValueConnector selectedConnector) { if (selectedConnector != null && selectedConnector.getConnectorValue() instanceof Module) { Module selectedModule = selectedConnector.getConnectorValue(); displayModule(workspaceName, selectedModule); // We do not reset displayed module on navigator selection anymore. // This is because when a node is selected in the tree at different // level, // the module connector selection is a 2-step process : // 1. deselection // 2. selection // The problem is that you never have from and to modules // simultaneously, // thus preventing complex algorithms in onEnter/onLeave actions. // } else { // displayModule(workspaceName, null); } } @SuppressWarnings("ConstantConditions") private Object[] synchWorkspaceNavigatorSelection(ICollectionConnectorListProvider navigatorConnector, Module module) { Object[] result = null; int moduleModelIndex = -1; for (ICollectionConnector childCollectionConnector : navigatorConnector.getCollectionConnectors()) { for (int i = 0; i < childCollectionConnector.getChildConnectorCount(); i++) { IValueConnector childConnector = childCollectionConnector.getChildConnector(i); if (module != null && module.equals(childConnector.getConnectorValue())) { moduleModelIndex = i; } if (childConnector instanceof ICollectionConnectorListProvider) { Object[] subResult = synchWorkspaceNavigatorSelection((ICollectionConnectorListProvider) childConnector, module); if (subResult != null) { result = subResult; } } } if (moduleModelIndex >= 0) { result = new Object[]{childCollectionConnector, moduleModelIndex}; } else { childCollectionConnector.setSelectedIndices(null, -1); } } return result; } private void translateModule(Module module) { module.setI18nName(getTranslation(module.getName(), getLocale())); module.setI18nDescription(getTranslation(module.getDescription(), getLocale())); module.setI18nHeaderDescription(getTranslation(module.getHeaderDescription(), getLocale())); if (module.getSubModules() != null) { for (Module subModule : module.getSubModules()) { translateModule(subModule); } } } /** * Translate workspace. * * @param workspace * the workspace */ protected void translateWorkspace(Workspace workspace) { workspace.setI18nName(getTranslation(workspace.getName(), getLocale())); workspace.setI18nDescription(getTranslation(workspace.getDescription(), "", getLocale())); workspace.setI18nHeaderDescription(getTranslation(workspace.getHeaderDescription(), "", getLocale())); workspace.setI18nPageHeaderDescription(getTranslation(workspace.getPageHeaderDescription(), "", getLocale())); if (workspace.getModules() != null) { for (Module module : workspace.getModules()) { translateModule(module); } } } /** * Gets the frameWidth. * * @return the frameWidth. */ protected Integer getFrameWidth() { return frameWidth; } /** * Sets the preferred application frame width. How this dimension is leveraged * depends on the UI channel. * * @param frameWidth * the frameWidth to set. */ public void setFrameWidth(Integer frameWidth) { this.frameWidth = frameWidth; } /** * Gets the frameHeight. * * @return the frameHeight. */ protected Integer getFrameHeight() { return frameHeight; } /** * Sets the preferred application frame height. How this dimension is * leveraged depends on the UI channel. * * @param frameHeight * the frameHeight to set. */ public void setFrameHeight(Integer frameHeight) { this.frameHeight = frameHeight; } /** * Gets the secondaryActionMap. * * @return the secondaryActionMap. */ @Override public ActionMap getSecondaryActionMap() { return secondaryActionMap; } /** * Assigns the view secondary action map. Same rules as the primary action map * apply except that actions in this map should be visually distinguished from * the main action map, e.g. placed in another toolbar. * * @param secondaryActionMap * the secondaryActionMap to set. */ public void setSecondaryActionMap(ActionMap secondaryActionMap) { this.secondaryActionMap = secondaryActionMap; } /** * Should auto login. * * @return the boolean */ protected boolean shouldAutoLogin() { UsernamePasswordHandler uph = getLoginCallbackHandler(); return uph.getPassword() != null && uph.getPassword().length() == 0; } /** * Encodes username / password into a string for storing. The stored string is * used later for "remember me" function. * * @param username * the user name. * @param password * the user password; * @return the encoded username/password string. */ @SuppressWarnings("UnusedParameters") protected String encodeUserPass(String username, String password) { String loginGuid = new RandomGUID().toString(); StringBuilder buff = new StringBuilder(); if (username != null) { buff.append(username); } buff.append(UP_SEP); if (password != null) { buff.append(loginGuid); } putUserPreference(getGlobalUserPreferenceGuidKey(username), loginGuid); return buff.toString(); } private String getGlobalUserPreferenceGuidKey(String username) { // We must encode the username in the preference key, since the user is not yet logged-in, // thus the preference store is the global one. return UP_GUID + "|" + (username != null ? username.toLowerCase() : null); } /** * Decodes username / password from a string for restoring into original * values. This is used in "remember me" function. * * @param encodedUserPass * the encoded username/password string. * @return an string array of username/password strings */ protected String[] decodeUserPass(String encodedUserPass) { String[] userPass = new String[2]; if (encodedUserPass != null) { String[] temp = encodedUserPass.split(UP_SEP); if (temp.length == 2) { userPass[0] = temp[0]; if (temp[1] != null && temp[1].equals(getUserPreference(getGlobalUserPreferenceGuidKey(userPass[0])))) { userPass[1] = ""; } } else if (temp.length == 1) { if (encodedUserPass.indexOf(UP_SEP) != 0) { userPass[0] = temp[0]; } } } return userPass; } /** * {@inheritDoc} */ @Override public IView<E> getCurrentModuleView() { IMapView<E> workspaceView = workspaceViews.get(getSelectedWorkspaceName()); if (workspaceView != null) { return workspaceView.getCurrentView(); } return null; } /** * Gets the client preferences store. * * @return the client preferences store. */ protected synchronized IPreferencesStore getClientPreferencesStore() { if (clientPreferencesStore == null) { clientPreferencesStore = createClientPreferencesStore(); clientPreferencesStore.setStorePath(getName()); } return clientPreferencesStore; } /** * Creates the clientPreferenceStore. * * @return the clientPreferenceStore. */ protected abstract IPreferencesStore createClientPreferencesStore(); /** * Sets the clientPreferenceStore. * * @param clientPreferencesStore * the clientPreferenceStore to set. */ public void setClientPreferencesStore(IPreferencesStore clientPreferencesStore) { this.clientPreferencesStore = clientPreferencesStore; } /** * Delegates to the backend controller. * <p> * {@inheritDoc} */ @Override public String getUserPreference(String key) { return getBackendController().getUserPreference(key); } /** * {@inheritDoc} */ @Override public void putUserPreference(String key, String value) { getBackendController().putUserPreference(key, value); } /** * {@inheritDoc} */ @Override public void removeUserPreference(String key) { getBackendController().removeUserPreference(key); } /** * Delegates to the backend controller. * <p> * {@inheritDoc} */ @Override public String getTranslation(String key, Locale locale) { return getBackendController().getTranslation(key, locale); } /** * Delegates to the backend controller. * <p> * {@inheritDoc} */ @Override public String getTranslation(String key, Object[] args, Locale locale) { return getBackendController().getTranslation(key, args, locale); } /** * Delegates to the backend controller. * <p> * {@inheritDoc} */ @Override public String getTranslation(String key, String defaultMessage, Locale locale) { return getBackendController().getTranslation(key, defaultMessage, locale); } /** * Delegates to the backend controller. * <p> * {@inheritDoc} */ @Override public String getTranslation(String key, Object[] args, String defaultMessage, Locale locale) { return getBackendController().getTranslation(key, args, defaultMessage, locale); } /** * {@inheritDoc} */ @Override public boolean isAccessGranted(ISecurable securable) { Map<String, Object> currentSecurityContext = getSecurityContext(); int snapshotsToRestore = 0; try { if (!currentSecurityContext.containsKey(SecurityContextConstants.WORKSPACE)) { pushToSecurityContext(getSelectedWorkspace()); snapshotsToRestore++; } if (!currentSecurityContext.containsKey(SecurityContextConstants.MODULE_CHAIN)) { pushToSecurityContext(getSelectedModule()); snapshotsToRestore++; } return getBackendController().isAccessGranted(securable); } finally { for (int i = 0; i < snapshotsToRestore; i++) { restoreLastSecurityContextSnapshot(); } } } /** * {@inheritDoc} */ @Override public Map<String, Object> getSecurityContext() { return getBackendController().getSecurityContext(); } /** * {@inheritDoc} */ @Override public ISecurityContextBuilder pushToSecurityContext(Object contextElement) { getBackendController().pushToSecurityContext(contextElement); return this; } /** * {@inheritDoc} */ @Override public ISecurityContextBuilder restoreLastSecurityContextSnapshot() { getBackendController().restoreLastSecurityContextSnapshot(); return this; } /** * Delegates to view factory. * <p> * {@inheritDoc} */ @Override public void focus(E component) { getViewFactory().focus(component); } /** * Delegates to view factory. * <p> * {@inheritDoc} */ @Override public void edit(E component) { getViewFactory().edit(component); } /** * Traces unexpected exceptions properly. * * @param ex * the exception to trace. */ @Override public void traceUnexpectedException(Throwable ex) { String sessionId = "[unknown session]"; String userId = "[unknown user]"; if (getApplicationSession() != null) { sessionId = getApplicationSession().getId(); userId = getApplicationSession().getUsername(); } LOG.error("An unexpected error occurred for user {} on session {}.", userId, sessionId, ex); } /** * Gets the checkActionThreadSafety. * * @return the checkActionThreadSafety. */ public boolean isCheckActionThreadSafety() { return checkActionThreadSafety; } /** * Sets the checkActionThreadSafety. * * @param checkActionThreadSafety * the checkActionThreadSafety to set. */ public void setCheckActionThreadSafety(boolean checkActionThreadSafety) { this.checkActionThreadSafety = checkActionThreadSafety; } /** * {@inheritDoc} */ @Override public final void displayModalDialog(E mainView, final List<G> actions, final String title, final E sourceComponent, final Map<String, Object> context, final Dimension dimension, boolean reuseCurrent) { displayDialog(mainView, actions, title, sourceComponent, context, dimension, reuseCurrent, true); } @Override public void displayUrl(String urlSpec) { displayUrl(urlSpec, UrlHelper.BLANK_TARGET); } }